package simulation;

import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;

import maze.Barcode;
import maze.Direction;
import maze.Location;
import maze.Maze;
import maze.Sector;

public class ColorMap {

	public BufferedImage[][] tileMap;
	private BufferedImage[][] distortionMap;

	private int tileMapWidth = 5;
	private int tileMapHeight = 5;
	private int tileSize = 40; //number of pixels width/height (currently 1 pixel/cm)
	private int sensorRadius = 1; //radius of the area readable by the lightsensor in units of pixels.
	public int distortion = 0; //percent of distortion. 0 = no distortion, 100 = pure random readings.

	//note to self: positions of pixels in bufferedimage have top left origin, y-down, x-right

	/**
	 * Initializes a new ColorMap.
	 * Please call setupMap(..) before using this class.
	 * Please set a proper sensorRadius (def = 1cm). 
	 * Set distortion if wanted.
	 */
	public ColorMap() {

		//testReadImage();

		//setupMap();
		//testGetSensorReading();

		//drawMap();		

	}

	public BufferedImage[][] getTileMap() {
		return tileMap;
	}

	/**
	 * Set the detection-radius of the lightsensor to given radius.
	 * @param radius units: cm
	 */
	public void SetSensorRadius(int radius) {
		sensorRadius = radius;
	}

	/**
	 * Needs setupMap() to be called first.
	 */
	public void testGetSensorReading() {
		int originalRadius = sensorRadius;
		sensorRadius = 10;

		System.out.println("Pos(80,20) expected: 255");
		System.out.println("calculated: " + GetSensorReading(80, 20));
		System.out.println(" ");
		System.out.println("Pos(180,180) expected: 0");
		System.out.println("calculated: " + GetSensorReading(180, 180));
		System.out.println(" ");
		System.out.println("Pos(RightMostTop) expected: 0");
		System.out.println("calculated: " + GetSensorReading(199, 199));
		System.out.println(" ");
		System.out.println("Pos(LeftMostTop) expected: 0");
		System.out.println("calculated: " + GetSensorReading(0, 199));
		System.out.println(" ");
		System.out.println("Pos(LeftMostBottom) expected: 255");
		System.out.println("calculated: " + GetSensorReading(0, 0));
		System.out.println(" ");
		System.out.println("Pos(RightMostBottom) expected: 255");
		System.out.println("calculated: " + GetSensorReading(199, 0));
		System.out.println(" ");
		System.out.println(" ");
		System.out.println("Pos(100,100) expected: near 128");
		System.out.println("calculated: " + GetSensorReading(100, 100));
		System.out.println(" ");
		System.out.println("Pos(3,20) expected: near 128");
		System.out.println("calculated: " + GetSensorReading(3, 20));
		System.out.println(" ");
		System.out.println("Pos(80,80) expected: near 191");
		System.out.println("calculated: " + GetSensorReading(80, 80));

		sensorRadius = originalRadius;
	}

	/**
	 * Returns the reading a lightsensor would detect being at given position.
	 * @param xPos - The xPos of the sensor (in cm)
	 * @param yPos - The yPos of the sensor (in cm)
	 * @return - sensor reading
	 */
	public int GetSensorReading(int xPos, int yPos) {
		if (xPos >= tileMapWidth * tileSize || yPos >= tileMapHeight * tileSize) {
			try {
				throw new Exception("Invalid position given");
			} catch (Exception e) {
			}
		}

		float pixelNb = 0;
		float pixelSum = 0;
		for (int i = 0; i < tileMapWidth; i++) {

			if (Math.abs(xPos - i * tileSize) < tileSize + sensorRadius) {
				for (int j = 0; j < tileMapHeight; j++) {

					if (Math.abs(yPos - j * tileSize) < tileSize + sensorRadius) {
						int relPosX = xPos - i * tileSize;
						int relPosY = yPos - j * tileSize;

						for (float k = 0.5f; k < tileSize; k++) {
							for (float l = 0.5f + tileSize - 1; l >= 0; l--) {
								/*
								 * voor elke pixel:
								 * als dist(pixel, relpos) < radius-0.5 => 100% grayValue
								 * als dist(pixel, relpos) > radius+0.5 => 0% grayValue
								 * anders 50% grayValue
								 */

								float dist = distance(relPosX, relPosY, k, l);
								if (dist <= sensorRadius - 0.5f) {
									pixelSum += calculateGrayValue(i, j, k, l);
									pixelNb++;
								} else if (dist >= sensorRadius + 0.5f) {
									//do nothing
								} else {
									float weight = 0.5f;

									pixelSum += weight
											* calculateGrayValue(i, j, k, l);
									pixelNb += weight;
								}
							}
						}
					}
				}
			}
		}
		return (int) ((pixelSum / pixelNb) + 0.5f);
	}

	/**
	 * Calculates the distorted value of the pixel at (pixelX,pixelY) in image (mapX,mapY)
	 */
	private int calculateGrayValue(int mapX, int mapY, float pixelX,
			float pixelY) {
		int undistortedValue = getGrayValue(tileMap[mapX][mapY],(int) pixelX,
				tileMap[mapX][mapY].getRaster().getHeight() - 1 - (int) pixelY);
		int distortionValue = getGrayValue(distortionMap[mapX][mapY],(int) pixelX, distortionMap[mapX][mapY].getRaster()
						.getHeight() - 1 - (int) pixelY);

		return (int)(((100 - distortion) * undistortedValue + distortion
				* distortionValue) *0.01f);
	}

	/**
	 * Returns the absolute distance between two points in 2D space.
	 * @param x1
	 * @param y1
	 * @param x2
	 * @param y2
	 * @return
	 */
	private float distance(float x1, float y1, float x2, float y2) {
		return (float) Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
	}

	private int getGrayValue(BufferedImage img, int xPos, int yPos) {
		int RGB = img.getRGB(xPos, yPos);
		int red = (RGB >> 16) & 255;
		int green = (RGB >> 8) & 255;
		int blue = (RGB) & 255;
		int gray = (int) (0.3f * red + 0.59f * green + 0.11 * blue + 0.5f);
		return gray;
	}

	private BufferedImage loadImage(String path) {
		BufferedImage img = null;
		try {
			img = ImageIO.read(new File(path));
		} catch (IOException e) {
		}

		return img;
	}

	/**
	 * Initializes a default map.
	 */
	public void setupMap() {

		/**
		 *  B  B  B  B B
		 *  B  B  B  B B
		 *  B  B B/W B B
		 *  W  W  W  W W
		 * B/W W  W  W W
		 */

		tileMap = new BufferedImage[5][5];

		try {
			String testTile01 = "..\\PenOTiles\\TestTile01.jpg";
			String whiteTile = "..\\PenOTiles\\TestTileWhite.jpg";
			String blackTile = "..\\PenOTiles\\TestTileBlack.jpg";

			tileMap[0][0] = loadImage(testTile01);
			tileMap[1][0] = loadImage(whiteTile);
			tileMap[2][0] = loadImage(whiteTile);
			tileMap[3][0] = loadImage(whiteTile);
			tileMap[4][0] = loadImage(whiteTile);
			tileMap[0][1] = loadImage(whiteTile);
			tileMap[1][1] = loadImage(whiteTile);
			tileMap[2][1] = loadImage(whiteTile);
			tileMap[3][1] = loadImage(whiteTile);
			tileMap[4][1] = loadImage(whiteTile);

			tileMap[0][2] = loadImage(blackTile);
			tileMap[1][2] = loadImage(blackTile);
			tileMap[2][2] = loadImage(testTile01);
			tileMap[3][2] = loadImage(blackTile);
			tileMap[4][2] = loadImage(blackTile);
			tileMap[0][3] = loadImage(blackTile);
			tileMap[1][3] = loadImage(blackTile);
			tileMap[2][3] = loadImage(blackTile);
			tileMap[3][3] = loadImage(blackTile);
			tileMap[4][3] = loadImage(blackTile);
			tileMap[0][4] = loadImage(blackTile);
			tileMap[1][4] = loadImage(blackTile);
			tileMap[2][4] = loadImage(blackTile);
			tileMap[3][4] = loadImage(blackTile);
			tileMap[4][4] = loadImage(blackTile);

		} catch (Exception ex) {
			System.out.println("File not found, ask Simon for the file");
		}
		
		setupDistortionMap();
	}

	/**
	 * Initializes a map from a given Maze.
	 * @param maze
	 * @param width The max number of sectors in the x direction, nul-based
	 * @param height The max number of sectors in the y direction, nul-based
	 */
	public void setupMap(Maze maze, int width, int height) {
		tileMap = new BufferedImage[width][height];
		tileMapWidth = width;
		tileMapHeight = height;

		for (int i = 0; i < width; i++) {
			for (int j = 0; j < height; j++) {

				String tileName = "";

				tileName += (maze.read(i, j, Direction.WEST, true) == 2) ? "0"
						: "1";
				tileName += ((maze.read(i, j, Direction.NORTH, true) == 2) ? "0"
						: "1");
				tileName += ((maze.read(i, j, Direction.EAST, true) == 2) ? "0"
						: "1");
				tileName += ((maze.read(i, j, Direction.SOUTH, true) == 2) ? "0"
						: "1");
				//System.out.println(tileName+ " " +i+ " "+j);

				try {
					tileMap[i][j] = ImageIO.read(new File("PenOTiles/"
							+ tileName + ".jpg"));
				} catch (IOException ex) {
					try {
						tileMap[i][j] = ImageIO.read(new File("..\\PenOTiles\\"
								+ tileName + ".jpg"));
					} catch (IOException e) {
						try {
							tileMap[i][j] = ImageIO.read(new File(
									"../PenOTiles/" + tileName + ".jpg"));
						} catch (IOException e1) {
							System.out.println("Could not load ColorMap file");
						}

					}
				}
			}
		}
		for (Barcode code : maze.getBarcodes()) {
			AddBarcode(code.getX(), code.getY(), code.getCode(),
					code.getDirection());
		}

		setupDistortionMap();
	}

	/**
	 * Adds a given barcode to the sector at given location in given direction.
	 * Note: the sector must exist!
	 * @param xPos
	 * @param yPos
	 * @param BC The barcode to add, 8 chars: 0(black) or 1(white)
	 * @param direction 0: left to right, 1: top to bottom
	 */
	public void AddBarcode(int xPos, int yPos, String BC, int direction) {
		BufferedImage img = tileMap[xPos][yPos];
		int offset = 0;
		int barWidth = 2; //width of the bars of the barcode (pixels)
		int bcStart = 12; //offset from the left/bottom (pixels)

		for (int b = 0; b < BC.length(); b++) {
			offset = b * barWidth;
			int RGB = -16777216;
			if (BC.charAt(b) == '1')
				RGB = -1;

			for (int i = 0; i < tileSize; i++) {

				if (direction == 0) {
					img.setRGB(bcStart + offset, i, RGB);
					img.setRGB(bcStart + offset + 1, i, RGB);
				}
				if (direction == 1) {
					img.setRGB(i, bcStart + offset, RGB);
					img.setRGB(i, bcStart + offset + 1, RGB);
				}

			}
		}

	}

	private void setupDistortionMap() {
		distortionMap = new BufferedImage[tileMapWidth][tileMapHeight];

		ArrayList<BufferedImage> files = new ArrayList<BufferedImage>();
		try {
			files.add(ImageIO.read(new File(
					"..\\PenOTiles\\RandomDistortion01.jpg")));
			files.add(ImageIO.read(new File(
					"..\\PenOTiles\\RandomDistortion02.jpg")));
			files.add(ImageIO.read(new File(
					"..\\PenOTiles\\RandomDistortion03.jpg")));
		} catch (IOException e) {
			try {
				files.add(ImageIO.read(new File(
						"PenOTiles/RandomDistortion01.jpg")));
				files.add(ImageIO.read(new File(
						"PenOTiles/RandomDistortion02.jpg")));
				files.add(ImageIO.read(new File(
						"PenOTiles/RandomDistortion03.jpg")));
			} catch (IOException e1) {
				try {
					files.add(ImageIO.read(new File(
							"../PenOTiles/RandomDistortion01.jpg")));
					files.add(ImageIO.read(new File(
							"../PenOTiles/RandomDistortion02.jpg")));
					files.add(ImageIO.read(new File(
							"../PenOTiles/RandomDistortion03.jpg")));
				} catch (IOException e2) {
					System.out
							.println("Could not load ColorMap Distortion file");
				}
			}
		}

		int random = 0;
		for (int i = 0; i < tileMapWidth; i++) {
			for (int j = 0; j < tileMapHeight; j++) {
				random = (int) (Math.random() * files.size());
				distortionMap[i][j] = files.get(random);
			}
		}
	}

	public void testReadImage() {
		BufferedImage img = null;
		try {
			img = ImageIO.read(new File("..\\PenOTiles\\TestTile.jpg"));
		} catch (IOException e) {
		}

		System.out.println("- Black:");
		System.out.println(img.getRGB(0, 0));
		printRGB(img.getRGB(0, 0));
		printGrayScale(img.getRGB(0, 0));
		System.out.println("- 50% Gray:");
		printRGB(img.getRGB(39, 0));
		printGrayScale(img.getRGB(39, 0));
		System.out.println("- White:");
		System.out.println(img.getRGB(0, 39));
		printRGB(img.getRGB(0, 39));
		printGrayScale(img.getRGB(0, 39));
		System.out.println("-----------");
	}

	private void printRGB(int RGB) {
		int red = (RGB >> 16) & 255;
		int green = (RGB >> 8) & 255;
		int blue = (RGB) & 255;

		System.out.println("R:" + red + " G:" + green + " B:" + blue);
	}

	private void printGrayScale(int RGB) {
		int red = (RGB >> 16) & 255;
		int green = (RGB >> 8) & 255;
		int blue = (RGB) & 255;
		int gray = (int) (0.3f * red + 0.59f * green + 0.11 * blue + 0.5f);

		System.out.println("GrayScale: " + gray);
	}

	public void testAddBarcode() {
		BufferedImage img = loadImage("..\\PenOTiles\\0000.jpg");
		setupMap();
		tileMap[2][2] = img;

		AddBarcode(2, 2, "01010110", 1);

		showImage(tileMap[2][2]);

		System.out.println(getGrayValue(tileMap[2][2], 20, 20));
	}

	public void showImage(BufferedImage img) {
		JLabel label = new JLabel(new ImageIcon(img));
		JFrame f = new JFrame();
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.getContentPane().add(label);
		f.pack();
		f.setLocation(200, 200);
		f.setVisible(true);
	}

	public void TestReadValueOverlap() {
		/**
		 * Test Maze:
		 * 
		 *  _ _ _
		 * |  _  |
		 * | |_| |
		 * |_ _ _|
		 * 
		 * 2121212
		 * 1221221
		 * 1211121
		 * 1121211
		 */
		char mazedata[][] = { { 2, 1, 2, 1, 2, 1, 2 }, { 1, 2, 2, 1, 2, 2, 1 },
				{ 1, 2, 1, 1, 1, 2, 1 }, { 1, 1, 2, 1, 2, 1, 1 } };

		Maze maze = new Maze(mazedata);

		setupMap(maze, 3, 3);

		showImage(tileMap[0][0]);

		System.out.println("Expected: 194 leftbrown");
		System.out.println(GetSensorReading(0, 20));
		System.out.println("Expected: 194 downbrown");
		System.out.println(GetSensorReading(20, 0));
		System.out.println("Expected: 194 rightbrown");
		System.out.println(GetSensorReading(120, 20));
		System.out.println("Expected: 194 upbrown");
		System.out.println(GetSensorReading(20, 120));
		System.out.println("Expected: 255 leftwhite");
		System.out.println(GetSensorReading(0, 40));
		System.out.println("Expected: 255 rightwhite");
		System.out.println(GetSensorReading(80, 20));
		System.out.println("Expected: 255 upwhite");
		System.out.println(GetSensorReading(20, 40));
		System.out.println("Expected: 255 downwhite");
		System.out.println(GetSensorReading(20, 80));

	}
}
